package net.luohuasheng.bee.proxy.jdbc.statement;

import net.luohuasheng.bee.proxy.jdbc.LoggingProxy;
import org.apache.commons.lang3.time.DateFormatUtils;

import java.sql.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @author luohuasheng
 */
public class StatementProxy<S extends Statement> implements Statement {

    private Long start;
    private boolean isNewOpen = false;

    protected static ThreadLocal<AtomicInteger> EFFECT_ROW = new ThreadLocal<>();
    protected static ThreadLocal<Boolean> IS_IGNORE = new ThreadLocal<>();
    private final List<String> sqls = new ArrayList<>();
    private final List<List<Object>> variables = new ArrayList<>();
    private long duration = 0;

    private final LoggingProxy.LogCall logCall;
    protected S statement;

    public StatementProxy(S statement) {
        this.statement = statement;
        this.logCall = LoggingProxy.get();
        if (!selfIgnore()) {
            EFFECT_ROW.set(new AtomicInteger());
        }
    }

    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
        return executeProxy(() -> new ResultSetProxy<>(statement.executeQuery(sql), LoggingProxy.get(), IS_IGNORE.get()));
    }


    @Override
    public int executeUpdate(String sql) throws SQLException {
        return executeProxy(() -> statement.executeUpdate(sql));
    }


    @Override
    public void close() throws SQLException {
        statement.close();
        if (!selfIgnore()) {
            LoggingProxy.addLogCall(logCall);
            duration = (System.currentTimeMillis() - start);
            LoggingProxy.print("Duration:" + duration + "ms", true);
            LoggingProxy.removeLogCall();
        }
    }

    @Override
    public int getMaxFieldSize() throws SQLException {
        return statement.getMaxFieldSize();
    }


    @Override
    public void setMaxFieldSize(int max) throws SQLException {
        statement.setMaxFieldSize(max);
    }

    @Override
    public int getMaxRows() throws SQLException {
        return statement.getMaxRows();
    }


    @Override
    public void setMaxRows(int max) throws SQLException {
        statement.setMaxRows(max);
    }


    @Override
    public void setEscapeProcessing(boolean enable) throws SQLException {
        statement.setEscapeProcessing(enable);
    }


    @Override
    public int getQueryTimeout() throws SQLException {
        return statement.getQueryTimeout();
    }


    @Override
    public void setQueryTimeout(int seconds) throws SQLException {
        statement.setQueryTimeout(seconds);
    }


    @Override
    public void cancel() throws SQLException {
        statement.cancel();
    }


    @Override
    public SQLWarning getWarnings() throws SQLException {
        return statement.getWarnings();
    }


    @Override
    public void clearWarnings() throws SQLException {
        statement.clearWarnings();
    }


    @Override
    public void setCursorName(String name) throws SQLException {
        statement.setCursorName(name);
    }


    @Override
    public boolean execute(String sql) throws SQLException {
        return executeProxy(() -> statement.execute(sql));
    }


    @Override
    public ResultSet getResultSet() throws SQLException {
        return new ResultSetProxy<>(statement.getResultSet(), LoggingProxy.get(), IS_IGNORE.get());
    }


    @Override
    public int getUpdateCount() throws SQLException {
        return statement.getUpdateCount();
    }


    @Override
    public boolean getMoreResults() throws SQLException {
        return statement.getMoreResults();
    }


    @Override
    public void setFetchDirection(int direction) throws SQLException {
        statement.setFetchDirection(direction);
    }


    @Override
    public int getFetchDirection() throws SQLException {
        return statement.getFetchDirection();
    }


    @Override
    public void setFetchSize(int rows) throws SQLException {
        statement.setFetchSize(rows);
    }


    @Override
    public int getFetchSize() throws SQLException {
        return statement.getFetchSize();
    }


    @Override
    public int getResultSetConcurrency() throws SQLException {
        return statement.getResultSetConcurrency();
    }


    @Override
    public int getResultSetType() throws SQLException {
        return statement.getResultSetType();
    }


    @Override
    public void addBatch(String sql) throws SQLException {
        statement.addBatch(sql);
        addSql(sql);
    }


    @Override
    public void clearBatch() throws SQLException {
        statement.clearBatch();
    }

    @Override
    public int[] executeBatch() throws SQLException {
        return executeProxy(() -> statement.executeBatch());
    }


    @Override
    public Connection getConnection() throws SQLException {
        return statement.getConnection();
    }


    @Override
    public boolean getMoreResults(int current) throws SQLException {
        return statement.getMoreResults();
    }


    @Override
    public ResultSet getGeneratedKeys() throws SQLException {
        return statement.getGeneratedKeys();
    }


    @Override
    public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        return executeProxy(() -> statement.executeUpdate(sql, autoGeneratedKeys));
    }


    @Override
    public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
        return executeProxy(() -> statement.executeUpdate(sql, columnIndexes));
    }


    @Override
    public int executeUpdate(String sql, String[] columnNames) throws SQLException {
        return executeProxy(() -> statement.executeUpdate(sql, columnNames));
    }


    @Override
    public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
        return executeProxy(() -> statement.execute(sql, autoGeneratedKeys));
    }


    @Override
    public boolean execute(String sql, int[] columnIndexes) throws SQLException {
        return executeProxy(() -> statement.execute(sql, columnIndexes));
    }


    @Override
    public boolean execute(String sql, String[] columnNames) throws SQLException {
        return executeProxy(() -> statement.execute(sql, columnNames));
    }

    @Override
    public int getResultSetHoldability() throws SQLException {
        return statement.getResultSetHoldability();
    }


    @Override
    public boolean isClosed() throws SQLException {
        return statement.isClosed();
    }


    @Override
    public void setPoolable(boolean poolable) throws SQLException {
        statement.setPoolable(poolable);
    }

    @Override
    public boolean isPoolable() throws SQLException {
        return statement.isPoolable();
    }


    @Override
    public void closeOnCompletion() throws SQLException {
        statement.closeOnCompletion();
    }

    @Override
    public boolean isCloseOnCompletion() throws SQLException {
        return statement.isCloseOnCompletion();
    }


    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return statement.unwrap(iface);
    }


    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return statement.isWrapperFor(iface);
    }

    public static void isIgnore(boolean isIgnore) {
        IS_IGNORE.set(isIgnore);
    }


    private static Boolean selfIgnore() {
        Boolean isIgnore;
        if ((isIgnore = IS_IGNORE.get()) == null) {
            return false;
        }
        return isIgnore;
    }

    public static void addRow() {
        if (!selfIgnore()) {
            EFFECT_ROW.get().incrementAndGet();
        }
    }

    public void setNewOpen(boolean newOpen) {
        this.isNewOpen = newOpen;
    }

    public void addVariables(Object variable) {
        if (!selfIgnore()) {
            List<Object> values = null;
            if (isNewOpen || variables.isEmpty()) {
                variables.add(new ArrayList<>());
                isNewOpen = false;
            }
            values = variables.get(variables.size() - 1);
            values.add(variable);
        }
    }

    public void addSql(String sql) {
        if (!selfIgnore()) {
            sqls.add(sql);
        }
    }


    public static void addRow(Integer count) {
        EFFECT_ROW.get().addAndGet(count);
    }

    static Integer getRow2() {
        if (EFFECT_ROW.get() == null) {
            return -1;
        } else {
            return EFFECT_ROW.get().get();
        }
    }

    public static Integer getRow() {
        if (EFFECT_ROW.get() == null) {
            return -1;
        } else {
            Integer row = EFFECT_ROW.get().get();
            EFFECT_ROW.remove();
            IS_IGNORE.remove();
            return row;
        }
    }

    protected <V> V executeProxy(ValueRunnable<V> runnable) throws SQLException {
        start = System.currentTimeMillis();
        try {
            if (sqls.size() == 1 && (variables.size() == 1 || variables.size() == 0)) {
                if (LoggingProxy.get() == null) {
                    LoggingProxy.print("Preparing:" + streamlineSql(sqls.get(0)), true);
                }
                if (variables.size() == 1) {
                    if (LoggingProxy.get() == null) {
                        LoggingProxy.print("Parameters:" + joinVariables(variables.get(0)), true);
                    }
                    LoggingProxy.print("Original:" + replaceVariables(sqls.get(0), variables.get(0)), true);
                }
            } else {
                if (sqls.size() > 0) {
                    LoggingProxy.print("Batching", true);
                }
                if (sqls.size() == 1) {
                    if (LoggingProxy.get() == null) {
                        LoggingProxy.print("Preparing:" + streamlineSql(sqls.get(0)), true);
                    }
                    for (List<Object> variable : variables) {
                        if (LoggingProxy.get() == null) {
                            LoggingProxy.print("Parameters:" + joinVariables(variable), true);
                        }
                        LoggingProxy.print("Original:" + replaceVariables(sqls.get(0), variable), true);
                    }
                } else {
                    for (int i = 0; i < variables.size(); i++) {
                        String sql = sqls.get(i);
                        if (LoggingProxy.get() == null) {
                            LoggingProxy.print("Preparing:" + streamlineSql(sql), true);
                        }
                        if (LoggingProxy.get() == null) {
                            LoggingProxy.print("Parameters:" + joinVariables(variables.get(i)), true);
                        }
                        LoggingProxy.print("Original:" + replaceVariables(sqls.get(i), variables.get(i)), true);
                    }
                }
            }
            V v = runnable.run();
            int count = statement.getUpdateCount();
            if (count != -1) {
                addRow(count);
            }

            return v;
        } finally {
            if (statement.getUpdateCount() != -1) {
                if (LoggingProxy.get() == null) {
                    LoggingProxy.print("Total:" + EFFECT_ROW.get(), false);
                }
                LoggingProxy.removeLogCall();
            }

        }
    }


    private String streamlineSql(String sql) {
        return sql.replaceAll("\n", " ").replaceAll("\\s+", " ");
    }

    private String replaceVariables(String sql, List<Object> variables) {
        List<String> replaces = new ArrayList<>();
        for (Object variable : variables) {
            if (variable == null) {
                replaces.add("null");
            } else if (variable instanceof Number || variable instanceof Boolean) {
                replaces.add(variable.toString());
            } else if (variable instanceof Date) {
                replaces.add("\"" + DateFormatUtils.format((Date) variable, "yyyy-MM-dd HH:mm:ss") + "\"");
            } else {
                replaces.add("\"" + variable.toString() + "\"");
            }
        }
        return streamlineSql(String.format(sql.replaceAll("%", "%%").replaceAll("\\?", "%s"), replaces.toArray()));
    }

    private String joinVariables(List<Object> list) {
        return list.stream().map(variable -> variable == null ? null : variable + "(" + variable.getClass().getSimpleName() + ")").collect(Collectors.joining(" , "));
    }


    protected interface ValueRunnable<V> {
        /**
         * 执行方法
         *
         * @return 执行结果
         * @throws SQLException 数据库异常
         */
        V run() throws SQLException;
    }


}
